---
title: nuxtjs
date: 2023-08-22 22:10:43
layout: PostBanner 
images: ['https://kodo.huixiangwuyou.com/blog/images/nuxtjs.jpeg']
tags: 
 - web
---

## Nuxt.js

Nuxt.js 是一个基于 Vue.js 的渐进式框架，用于构建现代 Web 应用程序。它简化了 Vue.js 应用的开发流程，特别适合构建：
- 服务端渲染(SSR)应用
静态站点生成(SSG)应用
单页应用(SPA)
混合应用

详细介绍可以参考官网: [nuxtjs 中文](https://www.nuxtjs.cn/)、[nuxtjs 英文(推荐)](https://nuxt.com/)
确保已安装 Node.js (建议 v14+) 和 npm/yarn
### nextjs的安装
```bash
npx create-nuxt-app my-project

What is your project named? my-app
Would you like to use Programming language(JavaScript/TypeScript)? 
Would you like to use Package manager (npm/yarn/pnpm) 
Would you like to use  CSS?  ui框架
...

{
  "scripts": {
    "dev": "nuxt", // 服务开发
    "build": "nuxt build", // 构建打包项目
    "start": "nuxt start", // 启动项目
    "generate": "nuxt generate" // 静态站点生成
  }
}
```

### nextjs的APP路由目录结构
```bash
my-project
├── .nuxt
├── node_modules 
├── components 组件 
├── assets 
├── composables  定义全局函数方法 
├── layouts  定义 NuxtLayout  文件夹下的 .vue文件对应路由的layout
├── pages  定义页面路由 
├── middleware 定义全局中间件 
├── modules 定义全局的注册模块
├── pages 定义页面路由
│   ├── /page1 其他跳转的页面文件夹作为路由
│   │   ├── page2.vue 
│   │   └── page3.vue 
│   ├── /page2 
│   │   ├── page4.vue
│   │   └── [id].vue 路径为 /page2/[id] 的页面
│   ├── /(page2)   用 （） 包裹表示这是某一个模块的路径，但是不读取括号内容作为路径一部分
│   │   ├── /@(id) 路径为 /page2/@(id) 的页面
│   │   └── /(..) photos\[id] 路径为 /page2/.. 的页面
│   ├── /dashboard 
│   │   ├── /[...slug]   表示 /dashboard/[...slug] 任意路由页面的动态路由，避免了dashboard路由下的404页面
│   │   └── /[id] 路径为 /page2/[id] 的页面
│   ├── layout.js
│   ├── app.config.ts
│   └── app.vue
├── server   和 nextjs 的api文件是一样的作为项目api接口
│   └── auth.ts
├── public
│   └── favicon.ico
├── plugins
│   └── nuxt.config.ts  
├── README.md
├── next.config.js
├── package.json
└── tsconfig.json
```

> layouts 文件夹下定义的文件会作为路由的布局文件，例如 `layout.js` 会作为所有路由的布局文件，`layout/default.js` 会作为默认路由的布局文件，`layout/blog.js` 会作为 `/blog` 路由的布局文件。

```js
<script setup lang="ts">
function enableCustomLayout () {
  setPageLayout('custom')
}
definePageMeta({
  layout: false,
});
</script>

<template>
  <div>
    <button @click="enableCustomLayout">Update layout</button>
  </div>
</template>
```
### 生命周期
> Server
- Nitro 启动：会初始化并执行该目录下的插件/server/plugins `执行一次`
- 初始化 Nitro：server/middleware/每个请求都会执行下面的中间件
- 创建 Vue 和 Nuxt 实例
- Route Validation：初始化插件之后、执行中间件之前
- 执行Nuxt App中间件
- 设置页面和组件： Nuxt 在此步骤中初始化页面及其组件，并使用useFetch和获取所有所需数据useAsyncData。Vue 生命周期钩子（例如onBeforeMount、onMounted和后续钩子）不会在 SSR 期间执行。
- 渲染并生成 HTML 输出

[lifecycle](https://nuxt.com/docs/guide/concepts/nuxt-lifecycle)
> Client
- 初始化 Nuxt 并执行 Nuxt App 插件
- Route Validation
- 执行Nuxt App中间件
- Mount Vue Application and Hydration
- Vue 生命周期
```js
// plugins/global-hooks.js
export default defineNuxtPlugin((nuxtApp) => {
  nuxtApp.hook('app:created', () => {
    console.log('Nuxt app has been created');
  });
  nuxtApp.hook('page:finish', () => {
    console.log('Page has finished rendering');
  });
  nuxtApp.hook('route:before', (to, from) => {
    console.log('Navigating to:', to.fullPath);
  });
});
```
[hooks 详情](https://nuxt.com/docs/api/advanced/hooks)
### 页面

```vue
<script lang="tsx" setup>
// Component could be a simple function with JSX syntax
const Welcome = () => <span>Welcome </span>

// Or using defineComponent setup that returns render function with JSX syntax
const Nuxt3 = defineComponent(() => {
  return () => <span class="text-(--ui-primary) font-bold">Nuxt 3</span>
})

// We can combine components with JSX syntax too
const InlineComponent = () => (
  <div>
    <Welcome />
    <span>to </span>
    <Nuxt3 />
  </div>
)
</script>

<template>
  <NuxtExample
    dir="advanced/jsx"
    icon="i-simple-icons-react"
  >
    <InlineComponent />
    <!-- Defined in components/jsx-component.ts -->
    <MyComponent message="This is an external JSX component" />
  </NuxtExample>
</template>
```
#### 布局组件
```
<!-- layouts/default.vue -->
<template>
  <div>
    <header>网站头部</header>
    <Nuxt /> <!-- 页面内容会渲染在这里 -->
    <footer>网站底部</footer>
  </div>
</template>
```




### 渲染模式
 主要分为两种渲染模式：服务端渲染（SSR）和客户端渲染（CSR）
```
// nuxt.config.js 全局禁用 SSR
export default {
  ssr: false, // 整个应用变为 CSR
}

<script>
export default {
  ssr: false, // 仅此页面不进行 SSR
}
</script>

// 动态SSR
<script  setup>
asyncData({ isServer }) {
  if (isServer) {
    // 服务端逻辑
  } else {
    // 客户端逻辑
  }
},
mounted() {
  if (process.client) {
    // 仅客户端执行的代码
  }
}
</script>
```
> 控制组件渲染
```
<template>
  <div>
    <ClientOnly>
      <ThirdPartyComponent /> <!-- 这个组件只在客户端渲染 -->
    </ClientOnly>
  </div>
</template>

<script setup>
import ThirdPartyComponent from '~/components/ThirdPartyComponent.vue'
</script>

// 或者通过状态判断
if (process.client) {
  // 客户端专用代码
}
```



### 路由方式
在项目中，app下面的每个文件夹就是路由的一个路径（用括号包裹作为模块的除外）
```js
// nuxt.config.ts
export default defineNuxtConfig({
  routeRules: {
    // Homepage pre-rendered at build time
    '/': { prerender: true },
    // Products page generated on demand, revalidates in background, cached until API response changes
    '/products': { swr: true },
    // Product pages generated on demand, revalidates in background, cached for 1 hour (3600 seconds)
    '/products/**': { swr: 3600 },
    // Blog posts page generated on demand, revalidates in background, cached on CDN for 1 hour (3600 seconds)
    '/blog': { isr: 3600 },
    // Blog post page generated on demand once until next deployment, cached on CDN
    '/blog/**': { isr: true },
    // Admin dashboard renders only on client-side
    '/admin/**': { ssr: false },
    // Add cors headers on API routes
    '/api/**': { cors: true },
    // Redirects legacy urls
    '/old-page': { redirect: '/new-page' }
  }
})
```

#### 动态路由
在路由中，可以使用方括号`app/dashboard/[...slug]`来表示动态路由参数。例如，`/dashboard/[slug]`表示一个动态路由，其中`slug`是一个动态参数。当访问`/dashboard/page/a`时，`slug`的值为`page/a`。
也可以通过`app/page/[id]`方式定义动态路由,例如，`/page/[id]`表示一个动态路由，其中`id`是一个动态参数。当访问`/page/123`时，`id`的值为`123`。

#### 嵌套路由
在嵌套路由中，在父路由下的文件夹。这种情况下，父路由和子路由都可以独立地渲染和导航。例如商品列表和商品详情`product/list`和`product/detail`中，路由直接互不影响（当然管理后台类型的除外）


### 数据请求
默认项目的数据请求适用`fetch`方法，但是也可以使用`@nuxt/axios`(和axios一样需要自行封装)等库进行数据请求。
```js
// server/api/submit.js
export default defineEventHandler(async (event) => {
  const body = await readBody(event)
  return { body }
})

//页面使用
<script setup lang="ts">
async function submit() {
  const { body } = await $fetch('/api/submit', {
    method: 'post',
    body: { test: 123 }
  })
}
</script>
```
**@nuxt/axios使用**
```js
// 安装 @nuxtjs/axios
npm install @nuxtjs/axios

// nuxt.config.js
export default {
  modules: [
    '@nuxtjs/axios'
  ],
  axios: {
    baseURL: 'https://api.example.com'
  }
}

<script>
export default {
  async asyncData({ params }) {
    const { data } = await axios.get(`/posts/${params.id}`)
    return { post: data }
  }
}
</script>
```

####  数据获取策略

#### 数据缓存


<br/>


### Middleware
中间件是一种在请求和响应之间执行的函数，可以用于处理请求、响应、路由等。在 Next.js 中，可以使用中间件来处理路由、请求、响应等。
`middleware.js 文件必须是在根目录下middleware文件夹,否则可能不响应`
```js
export default defineNuxtRouteMiddleware((to, from) => {
  if (to.params.id === '1') {
    return abortNavigation()
  }
  // In a real app you would probably not redirect every route to `/`
  // however it is important to check `to.path` before redirecting or you
  // might get an infinite redirect loop
  if (to.path !== '/') {
    return navigateTo('/')
  }
})

// Adding Middleware Dynamically
export default defineNuxtPlugin(() => {
  addRouteMiddleware('global-test', () => {
    console.log('this global middleware was added in a plugin and will be run on every route change')
  }, { global: true })

  addRouteMiddleware('named-test', () => {
    console.log('this named middleware was added in a plugin and would override any existing middleware of the same name')
  })
})

```

### UI组件
nextjs内置了UI组件库，可以直接使用
- NuxtImg
- NuxtPage
- Teleport
- NuxtPicture
- ClientOnly
- NuxtLink
- NuxtLayout

也可以使用官方推荐第三方UI组件库，[element-plus](https://element-plus.org/zh-CN/guide/ssr.html)、[templates](https://nuxt.com/templates)、[ui.nuxt](https://ui.nuxt.com/pro?utm_source=nuxt-website&utm_medium=header)

### 插件
```
// plugins/my-plugin.js
export default (context, inject) => {
  inject('myPlugin', {
    sayHello() {
      console.log('Hello from plugin!')
    }
  })
}

// nuxt.config.js
export default {
  plugins: [
    '~/plugins/my-plugin.js'
  ]
}

// 在页面中使用
this.$myPlugin.sayHello()
```


## nuxt.config.ts
```js
export default defineNuxtConfig({
  extends: [
    '@nuxt/examples-ui',
    './ui',
    './base',
  ],
  runtimeConfig: {
    public: {
      theme: {
        primaryColor: 'user_primary',
      },
    },
  },

  compatibilityDate: '2024-04-03',
})
```
[详情查看](https://nuxt.com/docs/api/nuxt-config)

## 常用 API
### useAppConfig
获取App的配置信息
```js
const appConfig = useAppConfig()

console.log(appConfig)
```

### useAsyncData
```js
<script setup lang="ts">
const { data, status, error, refresh, clear } = await useAsyncData(
  'mountains',
  () => $fetch('https://api.nuxtjs.dev/mountains')
)
</script>

// 监听参数
<script setup lang="ts">
const page = ref(1)
const { data: posts } = await useAsyncData(
  'posts',
  () => $fetch('https://fakeApi.com/posts', {
    params: {
      page: page.value
    }
  }), {
    watch: [page]
  }
)

// 监听路由
<script setup lang="ts">
const route = useRoute()
const userId = computed(() => `user-${route.params.id}`)

// When the route changes and userId updates, the data will be automatically refetched
const { data: user } = useAsyncData(
  userId,
  () => fetchUserById(route.params.id)
)
</script>

</script>

```

### useCookie
```
<script setup lang="ts">
const counter = useCookie('counter')

counter.value = counter.value || Math.round(Math.random() * 1000)
</script>
```
### useFetch
```js
<script setup lang="ts">
const { data, status, error, refresh, clear } = await useFetch('/api/modules', {
  pick: ['title']
})
</script>

const { data, status, error, refresh, clear } = await useFetch('/api/auth/login', {
  onRequest({ request, options }) {
    // Set the request headers
    // note that this relies on ofetch >= 1.4.0 - you may need to refresh your lockfile
    options.headers.set('Authorization', '...')
  },
  onRequestError({ request, options, error }) {
    // Handle the request errors
  },
  onResponse({ request, response, options }) {
    // Process the response data
    localStorage.setItem('token', response._data.token)
  },
  onResponseError({ request, response, options }) {
    // Handle the response errors
  }
})

<script setup lang="ts">
const route = useRoute()
const id = computed(() => route.params.id)

// When the route changes and id updates, the data will be automatically refetched
const { data: post } = await useFetch(() => `/api/posts/${id.value}`)
</script>

```

### useHead
```js
useHead(meta: MaybeComputedRef<MetaObject>): void

interface MetaObject {
  title?: string
  titleTemplate?: string | ((title?: string) => string)
  base?: Base
  link?: Link[]
  meta?: Meta[]
  style?: Style[]
  script?: Script[]
  noscript?: Noscript[]
  htmlAttrs?: HtmlAttributes
  bodyAttrs?: BodyAttributes
}

```
### useHeadSafe
```js
useHeadSafe({
  script: [
    { id: 'xss-script', innerHTML: 'alert("xss")' }
  ],
  meta: [
    { 'http-equiv': 'refresh', content: '0;javascript:alert(1)' }
  ]
})
// Will safely generate
// <script id="xss-script"></script>
// <meta content="0;javascript:alert(1)">
```

### useLazyAsyncData
```js
<script setup lang="ts">
/* Navigation will occur before fetching is complete.
  Handle 'pending' and 'error' states directly within your component's template
*/
const { status, data: count } = await useLazyAsyncData('count', () => $fetch('/api/count'))

watch(count, (newCount) => {
  // Because count might start out null, you won't have access
  // to its contents immediately, but you can watch it.
})
</script>
```

### useLazyFetch
```js
<script setup lang="ts">
/* Navigation will occur before fetching is complete.
 * Handle 'pending' and 'error' states directly within your component's template
 */
const { status, data: posts } = await useLazyFetch('/api/posts')
watch(posts, (newPosts) => {
  // Because posts might start out null, you won't have access
  // to its contents immediately, but you can watch it.
})
</script>

<template>
  <div v-if="status === 'pending'">
    Loading ...
  </div>
  <div v-else>
    <div v-for="post in posts">
      <!-- do something -->
    </div>
  </div>
</template>
```
### useNuxtApp
```js
const nuxtApp = useNuxtApp()
nuxtApp.provide('hello', (name) => `Hello ${name}!`)

// Prints "Hello name!"
console.log(nuxtApp.$hello('name'))
```

### useNuxtData
```js
<script setup lang="ts">
// Access to the cached value of useFetch in posts.vue (parent route)
const { data: posts } = useNuxtData('posts')

const route = useRoute()

const { data } = useLazyFetch(`/api/posts/${route.params.id}`, {
  key: `post-${route.params.id}`,
  default() {
    // Find the individual post from the cache and set it as the default value.
    return posts.value.find(post => post.id === route.params.id)
  }
})
</script>
```

### useRequestFetch
```js
<script setup lang="ts">
// This will forward the user's headers to the `/api/cookies` event handler
// Result: { cookies: { foo: 'bar' } }
const requestFetch = useRequestFetch()
const { data: forwarded } = await useAsyncData(() => requestFetch('/api/cookies'))

// This will NOT forward anything
// Result: { cookies: {} }
const { data: notForwarded } = await useAsyncData(() => $fetch('/api/cookies')) 
</script>
```

### useRouter
```js
<script setup lang="ts">
const router = useRouter()
router.back()
</script>
```

### useRoute
```js
<script setup lang="ts">
const route = useRoute()
const { data: mountain } = await useFetch(`/api/mountains/${route.params.slug}`)
</script>
```
### useSeoMeta
```
// app.vue
<script setup lang="ts">
useSeoMeta({
  title: 'My Amazing Site',
  ogTitle: 'My Amazing Site',
  description: 'This is my amazing site, let me tell you all about it.',
  ogDescription: 'This is my amazing site, let me tell you all about it.',
  ogImage: 'https://example.com/image.png',
  twitterCard: 'summary_large_image',
})
</script>

<script setup lang="ts">
const title = ref('My title')

useSeoMeta({
  title,
  description: () => `This is a description for the ${title.value} page`
})
</script>

<script setup lang="ts">
if (import.meta.server) {
  // These meta tags will only be added during server-side rendering
  useSeoMeta({
    robots: 'index, follow',
    description: 'Static description that does not need reactivity',
    ogImage: 'https://example.com/image.png',
    // other static meta tags...
  })
}

const dynamicTitle = ref('My title')
// Only use reactive meta tags outside the condition when necessary
useSeoMeta({
  title: () => dynamicTitle.value,
  ogTitle: () => dynamicTitle.value,
})
</script>

```

### useServerSeoMeta
```js
// app.vue
<script setup lang="ts">
useServerSeoMeta({
  robots: 'index, follow'
})
</script>
```

### useState
```js
// Create a reactive state and set default value
const count = useState('counter', () => Math.round(Math.random() * 100))

const state = useState('my-shallow-state', () => shallowRef({ deep: 'not reactive' }))
// isShallow(state) === true

```
